/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.service.time.ntptime;

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import cgl.narada.service.ServiceException;
import cgl.narada.service.ServiceProperties;
import cgl.narada.service.qos.impl.QosServiceImpl;

/**
 * This is the thread that computes time according to NTP and adjust the time in
 * NtpTimeserviceImpl class.
 *
 * @author Hasan Bulut  - hbulut@indiana.edu
 */
public class NtpTimeService extends Thread {
  //~ Static fields/initializers -----------------------------------------------

  private static NtpTimeService instance     = new NtpTimeService();
  private static boolean        checkLastUTC = false;

  //~ Instance fields ----------------------------------------------------------

  protected String         propertiesFile = "timeservice.properties";
  private DataOutputStream dumpTime    = null;
  private HashMap          serverList  = new HashMap();
  private LinkedList       serverIPs   = new LinkedList();
  private List             ntpInfoList = Collections.synchronizedList(new LinkedList());
  private NTP              ntp;
  private ServiceProperties      serviceProperties;
  private QosServiceImpl  qosServiceImpl;
  private String          dumpTimeFilename = "DumpTime.txt";
  private TimeServiceImpl tsi              = TimeServiceImpl.getInstance();
  private boolean         debugNTP         = false;
  private boolean         running          = true;
  private int             numberOfServers  = 0;
  private long            baseTime         = 0;
  private long            delta            = 0;
  private long            interval         = 15000; // msec

  //~ Constructors -------------------------------------------------------------

  /**
   * Generates a new NtpTimeService object.
   */
  private NtpTimeService() {
    //this.loadDefaults();
    setName("NtpTimeService");
  }

  private void initializeDumpTimeFile() {
    if (debugNTP) {
      if (dumpTimeFilename != null) {
        try {
          FileOutputStream fos = new FileOutputStream(dumpTimeFilename);
          if (fos != null) {
            dumpTime = new DataOutputStream(fos);
          }
        } catch (FileNotFoundException ex) {
          debugNTP = false;
          System.out.println("Debug Time property is disabled. Exception occured:\n" +
                             ex);
        }
        if (dumpTime == null) {
          debugNTP = false;
          System.out.println("Debug Time property is disabled. Unable to open file:" +
                             dumpTimeFilename);
        }
      }
    }
  }

  //~ Methods ------------------------------------------------------------------

  /**
   * Returns true if checkLastUTC is true, otherwise returns false. This enables
   * OffsetHandler to check the previous time provided before making any
   * modifications to the offset.
   *
   * @return boolean - value of checkLastUTC
   */
  public static boolean isCheckLastUTC() {
    return checkLastUTC;
  }

  /**
   * Gets the only instance of NtpTimeService.
   *
   * @return NtpTimeService - this object
   */
  public static NtpTimeService getInstance() {
    return instance;
  }

  /**
   * Gets samples from Ntp Servers. This method excahnges NtpDatagramPacket with
   * Ntp servers and sets NtpInfo object from the received NtpDatagramPackets.
   * Then NtpInfo objects are put into a queue to be processed by computeOffset()
   * to compute the offset.
   */
  public void getNtpSamples() {
    String ntpServer = "nist.time.gov";
    if (debugNTP) {
      System.out.println("\n---new sample set---");
    }
    for (int i = 0; i < serverIPs.size(); i++) {
      ntpServer = serverIPs.get(i).toString();
      NtpInfo ntpInfo = new NtpInfo();
      try {
        NtpConnection ntpConnection = new NtpConnection(InetAddress.getByName(ntpServer));
        ntpInfo = ntpConnection.getInfo();
        ntpConnection.close();
        if ((ntpInfo.originate < 1) || (ntpInfo.receive < 1)) {
          continue;
        }
        int n = Integer.parseInt(serverList.get(ntpInfo.serverAddress).toString());
        if (debugNTP) {
          System.out.println(n + "\t" + ntpInfo.reception + "\t" +
                             ntpInfo.roundTripDelay + "\t" + ntpInfo.offset);
        }
        addNtpInfo(ntpInfo);
      } catch (IOException ex) {
      }
    }
  }

  /**
   * gets the number of servers obtained from properties file.
   *
   * @return int - number of servers
   */
  public int getNumberOfServers() {
    return numberOfServers;
  }

  /**
   * Sets QosServiceImpl pointer to get a reference to the properties loaded at
   * initialization.
   *
   * @param qosServiceImpl QosServiceImpl
   */
  public void setQosServiceImpl(QosServiceImpl qosServiceImpl) {
    this.qosServiceImpl = qosServiceImpl;
    serviceProperties   = this.qosServiceImpl.getServiceProperties();
  }

  /**
   * Sets the value of running to the new value.
   *
   * @param running - new value of running. Setting this vlaue to false causes
   * thread to stop running
   */
  public void setRunning(boolean running) {
    this.running = running;
  }

  /**
   * Returns true if running is true, otherwise returns false.
   *
   * @return boolean - value of running
   */
  public boolean isRunning() {
    return running;
  }

  /**
   * Gets server list. serverList is a HashMap that maps server IP to an integer.
   * This integer is peer id.
   *
   * @return HashMap - server list
   */
  public HashMap getServerList() {
    return serverList;
  }

  /**
   * Adds NtpInfo object to the queue.
   *
   * @param ntpInfo NtpInfo - NtpInfo object to be added to the queue.
   */
  public void addNtpInfo(NtpInfo ntpInfo) {
    ntpInfoList.add(ntpInfo);
  }

  /**
   * Computes the offset from the NtpInfo obtained from servers available in the
   * list. It filters th eNtpInfo objects in the queue and calls dts() and
   * select() methods which are implemented in NTP class. It then computes the
   * new offset.
   */
  public void computeOffset() {
    delta = 0;
    for (int i = 0; i < ntpInfoList.size(); i++) {
      ntp.filter((NtpInfo) ntpInfoList.get(i));
    }
    ntp.dts();
    ntp.select();
    delta = (long) Math.round(ntp.getTheta());
    if (delta != 0) {
      delta    = -baseTime + (long) ntp.getTheta();
      baseTime = baseTime + delta;
      tsi.setOffset(delta);
    }
  }

  /**
   * This method is called inside run before the loop starts. It tries to set the
   * offset to a predetermined range. It attempts it until the accuracy is reached
   * or the counter is reached. accuracy and the counter values are set in the
   * properties file with the other information, i.e. interval, serverIPs, etc.
   * The default value for accuracy is 5 ms and for the counter is 5. It waits
   * 1000 ms between each iteration until the the offset is reached.
   *
   * @return boolean - it returns true if success, otherwise returns false;
   */
  public boolean initialize() {
    ntp = new NTP();
    ntp.initialize();
    boolean done    = false;
    int     counter = 0;
    long    temp    = (new Date()).getTime();
    long    temp1   = 0;
    while (!done) {
      this.getNtpSamples();
      this.computeOffset();
      this.ntpInfoList.clear();
      temp1 = (new Date()).getTime() - temp;
      if (temp1 > 3000) {
        done = true;
      }
      try {
        sleep(1000);
      } catch (InterruptedException ex) {
      }
    }
    System.out.println("Network Time Protocol: Computed Offset = " +
                       tsi.getOffset());
    ntp.initialize();
    return true;
  }

  /**
   * Reads Ntp server IPs, interval time and other required params from
   * DefaultParams class.
   */
  public void loadDefaults() {
    
    DefaultParams defaultValues = new DefaultParams();
    interval = defaultValues.getInterval();
    debugNTP = defaultValues.isDebugNTP();
    initializeDumpTimeFile();
    StringTokenizer servers = new StringTokenizer(defaultValues.getServerIPS(),
                                                  ",");
    while (servers.hasMoreTokens()) {
      String timeserver = servers.nextToken();
      String ts = timeserver.trim();
      serverIPs.add(ts);
      try {
        serverList.put(InetAddress.getByName(ts),
                       Integer.toString(numberOfServers));
      } catch (UnknownHostException ex) {
      }
      if (debugNTP) {
        System.out.println("time server id:" + numberOfServers +
                           "\ttime server IP:" + ts);
      }
      numberOfServers++;
    }
  }

  /**
   * Reads Ntp server IPs, interval time and other required params from
   * properties pointer obtained from QosServiceImpl.
   */
  public void loadProperties() {
    serverIPs.clear();
    serverList.clear();
    numberOfServers=0;
    DefaultParams defaultValues = new DefaultParams();
    if ((serviceProperties.getProperty("NTP_Debug").trim()).equalsIgnoreCase("ON")) {
      debugNTP = true;
    }
    if (debugNTP) {
      System.out.println("Debugger is ON");
    }
    interval = Long.parseLong(serviceProperties.getProperty("NTP_Interval")
                                               .trim());
    if ((interval <= 1000) || (interval >= 300000)) {
      interval = defaultValues.getInterval();
    }
    String serverNames = null;
    serverNames = serviceProperties.getProperty("NTP_Servers", serverNames);

    StringTokenizer servers = new StringTokenizer(serverNames, ",[]");

    try {
      while (servers.hasMoreTokens()) {
        String timeserver = servers.nextToken();
        String ts = timeserver.trim();
        serverIPs.add(ts);
        serverList.put(InetAddress.getByName(ts),
                       Integer.toString(numberOfServers));
        if (debugNTP) {
          System.out.println("server id:" + numberOfServers + "\tserver IP:" +
                             ts);
        }
        numberOfServers++;
      }
    } catch (UnknownHostException ex) {
      ex.printStackTrace();
      serverIPs.clear();
      serverList.clear();
      numberOfServers=0;
      servers = new StringTokenizer(defaultValues.getServerIPS(), ",");
      while (servers.hasMoreTokens()) {
        String timeserver = servers.nextToken();
        String ts = timeserver.trim();
        serverIPs.add(ts);
        try {
          serverList.put(InetAddress.getByName(ts),
                         Integer.toString(numberOfServers));
        } catch (UnknownHostException ex1) {
          ex.printStackTrace();
        }
        if (debugNTP) {
          System.out.println("time server id:" + numberOfServers +
                             "\ttime server IP:" + ts);
        }
        numberOfServers++;
      }
    }

    initializeDumpTimeFile();
  }

  /**
   * This method calls getNtpSamples() and computeOffset() in a while loop,
   * looping on running variable, which can be set to false by setRunning(false)
   * to break the loop.
   */
  public void run() {
    if (serviceProperties != null) {
      loadProperties();
    } else {
      loadDefaults();
    }
    
    this.checkLastUTC = false;
    ntp               = new NTP();
    //   System.out.println("calling initialize:"+(new Date()).getTime());
    boolean init = initialize();
    
    //   System.out.println("initialization called:"+(new Date()).getTime());
    this.checkLastUTC = true;
    LinkedList lnk     = new LinkedList();
    int        counter = 0;
    while (running) {
      int tempct = 0;
      while ((ntpInfoList.size() < (serverIPs.size() / 2)) && (tempct < 2)) {
	ntpInfoList.clear();
	this.getNtpSamples();
	tempct++;
      }
      if (ntpInfoList.size() == 0) {
	String temp = "Unable to receive NTP packets from any NTP servers ";
	temp += "provided. Please check your network connections or provide ";
	temp += "another set of NTP servers.\n";
	ServiceException se = new ServiceException(temp);
	tsi.setServiceException(se);
      } else {
	long time1 = (new NtpTimestamp()).getTimeInMilliSeconds();
	this.computeOffset();
	tsi.setServiceException(null);
	if (debugNTP) {
	  counter++;
	  lnk.add(Long.toString(time1));
	  lnk.add(Long.toString(baseTime));
	  lnk.add(Long.toString(delta));
	  if (counter % 2 == 0) {
	    for (int i = 0; i < lnk.size(); i++) {
	      String temp = lnk.get(i) + "\t" + lnk.get(i + 1) + "\t" +
		lnk.get(i + 2) + "\n";
	      try {
		dumpTime.writeBytes(temp);
	      } catch (IOException ex1) {
		ex1.printStackTrace();
	      }
	      i = i + 2;
	    }
	    lnk.clear();
	  }
	}
      }
      this.ntpInfoList.clear();
      try {
	sleep(interval);
      } catch (InterruptedException ex) {
      }
    }
  }

  public void terminateService() {
    interrupt();
    setRunning(false);
  }
}
